/* sprite-data.vala
 *
 * Copyright (C) 2010  Richard Talbot-Watkins
 * Copyright (C) 2010  Dave Jeffery
 * 
 *  This program is free software: you can redistribute it and/or modify
 *  it under the terms of the GNU General Public License as published by
 *  the Free Software Foundation, either version 3 of the License, or
 *  (at your option) any later version.
 *
 *  This program is distributed in the hope that it will be useful,
 *  but WITHOUT ANY WARRANTY; without even the implied warranty of
 *  MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 *  GNU General Public License for more details.
 *
 *  You should have received a copy of the GNU General Public License
 *  along with this program.  If not, see <http://www.gnu.org/licenses/>.
 * 
 * Authors:
 *  Richard Talbot-Watkins <richtw1@gmail.com>
 * 	Dave Jeffery <david.richard.jeffery@gmail.com>
 */

/**
 * Sprite Data Model
 *
 * The class is only responsible for storing and manipulating the byte 
 * array used to hold sprite data in the program.
 *
 * @author Richard Talbot-Watkins <richtw1@gmail.com>
 * @author Dave Jeffery <david.richard.jeffery@gmail.com>
 * @version 2010.1220
 * @since 0.1
 */
private class MonsterDesigner.SpriteData : GLib.Object {
	/**
	 * Signal emitted when data changed.
	 */
	public signal void changed ();

	/**
	 * Signal emitted when data about to change.
	 *
	 * Used undo/redo for history.
	 */
	public signal void will_change ();

	/**
	 *  Getter/Setter for sprite editing mode.
	 */
	private int _rows = 0;

	/**
	 * Number of 'rows' the sprite has.  
	 * 
	 * A row is a full character row, i.e. 8 pixels
	 */
	public int rows {
		get {
			return _rows;
		} 
		set {
			_rows = value;
		}
	}

	/**
	 * Convenience property to make code more readable.
	 */
	public int width {
		get {
			return SPRITE_WIDTH;
		}
	}

	/**
	 * Convenience property to make code more readable.
	 */
	public int height {
		get{
			return ROW_HEIGHT * _rows;
		}
	}

	/**
	 *  Fixed sprite width.
	 */
	public const int SPRITE_WIDTH = 12;

	/**
	 *  Fixed row height.
	 */
	public const int ROW_HEIGHT = 8;

	/**
	 * Byte array containing the sprite data.
	 */
	private uint8[] bitmap;


	/**
	 * Returns a clone of the sprite data array.
	 * 
	 * Used for undo and redo.
	 */
	public Clone clone () {
		return new Clone (bitmap);
	}

	/**
	 * Sets the sprite data array from a cloned copy.
	 *
	 * Used for undo and redo.
	 */
	public void set_from_clone (Clone clone) {
		bitmap = clone.data;
		_rows = bitmap.length / 96;

		/* Emit changed signal. */
		changed ();
	}

	/**
	 * Sets an area of the sprite data from the clipboard.
	 *
	 * @param start Top left corner of area to set.
	 * @param clipboard Clipboard data to copy into the area.
	 */
	public void set_area (Point start, Clipboard clipboard) {

		for (int y = 0; y != clipboard.height; ++y) {
			var sy = start.y + y;
			if (sy > ROW_HEIGHT * _rows) { break; }

			for (int x = 0; x != clipboard.width; ++x) {
				var sx = x + start.x;
				if (sx > SPRITE_WIDTH) { break; }

				bitmap[sy * SPRITE_WIDTH + sx] = 
				clipboard.contents [y * clipboard.width + x];
			}
		}

		/* Emit changed signal. */
		changed ();
	}

	/**
	 * Converts indexed sprite data into the Cairo RGB24 format. 
	 *
	 * The resulting array can be rendered on a Cairo ImageSurface easily.
	 * Note that RGB24 takes four bytes, in the order BGRA - the A value is 
	 * never used. Using RGB24 is easier than ARGB32, as ARGB32 values need 
	 * to be pre-multiplied.
	 *
	 * For historical reasons, vala's Cairo vapi specifies a uchar[] array
	 * instead of the expected uint8[] array for RGB24 data. This may be
	 * fixed in a future version, breaking this function.
	 * 
	 * @param palette_data BBC palette used to display sprite data
	 * @return the indexed_data array converted to Cairo's RGB24 format
	 */
	public uchar[] get_as_rgb24 (PaletteData palette_data) {
		/* Reset the rgb24_data array */
		uchar[] rgb24_data = {};
		foreach(uint8 byte in bitmap) {
			var c = palette_data.logical_to_cairo((int) byte);
			rgb24_data += (uchar) ((int) c.blue) * 255; // blue 
			rgb24_data += (uchar) ((int) c.green) * 255; // green
			rgb24_data += (uchar) ((int) c.red) * 255; // red
			rgb24_data += (uchar) 0xff; // alpha
		}
		return rgb24_data;
	}

	/**
	 * Sets sprite data from an array of bytes in BBC Micro Mode 5
	 * format. 
	 * 
	 * In MODE 5, each byte contains four 2:1 aspect ratio pixels each
	 * of which can be one of four logical colours. However, instead of
	 * being ordered sequentially across the screen and then down, the bytes
	 * are ordered in columns of eight bytes across the screen and then
	 * down, presumably to speed character rendering.
	 *
	 * @param bbc_data Array of sprite data in BBC Mode 5 format.
	 * @throws signal changed Emitted when sprite data is updated.
	 */
	public void set_from_bbc_data (uint8[] bbc_data) {
		rows = bbc_data.length / 24;
	    bitmap = new uint8[SPRITE_WIDTH * 8 * rows];

		var p = 0;

		for (int row = 0; row != rows; ++row) {
			for (int x = 0; x != 3; ++x) {
				for (int line = 0; line != 8; ++line) {
					int n = bbc_data[p++];
					for (int pix = 0; pix != 4; ++pix) {
						uint8 pixel = 0;
						if ((n & (8 >> pix)) != 0) { pixel |= 1; }
						if ((n & (128 >> pix)) != 0) { pixel |= 2; }
						bitmap[SPRITE_WIDTH * (row * 8 + line) + x * 4 + pix] 
						= pixel;
					}
				}
			}
		}

		/* Emit changed signal. */
		changed ();
	}

	/**
	 * Returns sprite data as an array of bytes in BBC Micro Mode 5
	 * format. See set_as_bbc_data for an explanation of the format.
	 * 
	 * @return array of sprite data in BBC MODE5 format
	 * @throws An IOError thrown if an illegal colour for MODE5 is found.
	 */
	public uint8[] get_as_bbc_data () throws IOError {

		uint8[] bbc_data = {};

		for (int y = 0; y != rows; ++y) {
			for (int x = 0; x != 3; ++x) {
				for (int l = 0; l != 8; ++l) {
					var beeb_byte = 0;
					for (int p = 0; p < 4; p++) {
						int xx = x * 4 + p;
						int yy = y * 8 + l;

						int bc = 0;
	                    switch (bitmap[yy * SPRITE_WIDTH + xx])
	                    {
	                        case 0: bc = 0x00; break;
	                        case 1: bc = 0x01; break;
	                        case 2: bc = 0x10; break;
	                        case 3: bc = 0x11; break;
	                        default: throw new IOError.ILLEGAL_COLOUR
								("Found illegal colour in sprite.");
	                	}
						beeb_byte |= (bc << (3 - p));
					}
					bbc_data += ((uint8) beeb_byte);
				}
			}
		}

		return bbc_data;
	}

	/**
	 * Returns an area of the sprite array in indexed format.
	 *
	 * Used to put data onto the clipboard.
	 * 
	 * @param box Area of sprite data to return.
	 * @return An indexed array containing the specified area of sprite data.
	 */
	public uint8[] get_area (Rectangle box) {
		uint8[] area = {};

		for (int y = box.y; y != box.y + box.height; ++y) {
			for (int x = box.x; x != box.x + box.width; ++x) {
				area += bitmap[x + SPRITE_WIDTH * y];
			}
		}

		return area;
	}

	/**
	 * Flips the sprite data horizontally in the specified area.
	 * 
	 * @param box Area of sprite data to flip.
	 */
	public void flip_horizontal (Rectangle box) {

		for (int y = box.y; y != box.y + box.height; ++y) {
			for (int x = 0; x != box.width / 2; ++x) {
				uint8 temp = bitmap[box.x + x + y * SPRITE_WIDTH];
				bitmap[box.x + x + y * SPRITE_WIDTH] = 
					bitmap[((box.x + (box.width - 1)) - x) + y * SPRITE_WIDTH];
				bitmap[((box.x + (box.width - 1)) - x) + y * SPRITE_WIDTH] = temp;
			}
		}

		/* Emit changed signal. */			
		changed ();
	}

	/**
	 * Flips the sprite data vertically in the specified area.
	 * 
	 * @param box Area of sprite data to flip.
	 */
	public void flip_vertical (Rectangle box) {
		for (int x = box.x; x != box.x + box.width; ++x) {
			for (int y = 0; y != box.height / 2; ++y) {
				uint8 temp = bitmap[x + (box.y + y) * SPRITE_WIDTH];
				bitmap[x + (box.y + y) * SPRITE_WIDTH] = 
					bitmap[x + ((box.y + (box.height - 1)) - y) * SPRITE_WIDTH];
				bitmap[x + ((box.y + (box.height - 1)) - y) * SPRITE_WIDTH] = temp;
			}
		}

		/* Emit changed signal. */			
		changed ();
	}

	/**
	 * Creates blank sprite data for a specified number of rows.
	 *
	 * @param rows the number of blank rows of sprite data to create.
	 */
	public void set_blank (int rows) {
		_rows = rows;
	    bitmap = new uint8[SPRITE_WIDTH * ROW_HEIGHT * rows];

		/* Emit changed signal. */			
		changed ();
	}

	/**
	 * Changes the number of rows of sprite data.
	 *
	 * @param rows The number of rows of sprite data.
	 */
	public void resize (int new_rows_value) {
		if (new_rows_value != rows)
		{
			/* Emit will change signal. */
			will_change ();

			bitmap.resize (new_rows_value * ROW_HEIGHT * SPRITE_WIDTH);
			_rows = new_rows_value;

			/* Emit changed signal. */			
			changed ();
		}
	}

	/**
	 * Returns a pixel in the sprite data as a Cairo RGB value.
	 *
	 * @param point The x and y position of the pixel in the sprite.
	 * @param palette_data Used to convert logical colour to physical colour.
	 */
	public CairoColour get_pixel (Point point, PaletteData palette_data) {
		var byte = bitmap[point.x + point.y * SPRITE_WIDTH];
		return palette_data.logical_to_cairo((int) byte);
	}

	public void fill (Point target, int logical_colour)
	{
		/* Check the fill target falls on a valid row. */
		if (target.x >= SPRITE_WIDTH) { return;} 
		if (target.y >= _rows * ROW_HEIGHT) { return; }

		/* Check we actually can fill something. */
		if (bitmap[target.x + target.y * SPRITE_WIDTH] 
								== logical_colour) { return; }

		/* Emit will change signal. */
		will_change ();

		/* Do the flood fill (recursive method). */
		process_fill (target.x, target.y, (uint8) logical_colour);

		/* Emit changed signal. */
		changed ();
	}

	/**
	 * I deliberately used int x and y instead of a Point struct for speed.
	 */
	private void process_fill (int x, int y, uint8 logical_colour) {
		/* Look at which colour we're flood filling over. */
		var this_pixel = bitmap[x + y * SPRITE_WIDTH];

		/* If it's equal to the current colour, do nothing. */
		if (this_pixel == logical_colour) { return; }

		/* Look left and right to find the flood fill limits for this row. */
		var leftmost = x;
		var rightmost = x;

		while (leftmost > 0 && 
				bitmap[leftmost - 1 + y * SPRITE_WIDTH]	== this_pixel) {
			leftmost--;
		}

		while (rightmost < SPRITE_WIDTH - 1 &&
				bitmap[rightmost + 1 + y * SPRITE_WIDTH] == this_pixel) {
			rightmost++;
		}

		/* And Fill in the row. */
		for (int i = leftmost; i <= rightmost; i++) {
			bitmap[i + y * SPRITE_WIDTH] = logical_colour;
		}

		/* Now look at the row above and below. */
		for (int dir = -1; dir <= 1; dir += 2) {
			/* If we are still within the sprite bounds. */
			if (y + dir >= 0 && 
				y + dir < _rows * ROW_HEIGHT) {
				/* Scan along between the limits of the row just filled. */
				for (x = leftmost; x <= rightmost; x++) {
					/* If we find a pixel we need to fill into... */
					if (bitmap[x + (y + dir) * SPRITE_WIDTH] == this_pixel) {
						/* ...find the end of this section... */
						int this_x = x;
						while (x < rightmost &&
								bitmap[x + 1 + (y + dir) * SPRITE_WIDTH] 
								== this_pixel) {
							x++;
						}
						/*...and recursively call the fill algorithm. */
						process_fill (this_x, y + dir, logical_colour);
					}
				}
			}
		}
	}

	public void insert_row (int y, int logical_colour)
	{
		/* Check the y parameter falls on a valid row. */
		if (y >= _rows * ROW_HEIGHT) { return; }

		/* Emit will change signal. */
		will_change ();

		/* Shunt everything forwards a row in the bitmap. */
		for (int yy = _rows * ROW_HEIGHT - 2; yy >= y; yy--) {
			for (int x = 0; x != SPRITE_WIDTH; ++x) {
				bitmap[x + (yy + 1) * SPRITE_WIDTH] = 
				bitmap[x + yy * SPRITE_WIDTH];
			}
		}

		/* And clear the row we just made. */
		var c = (uint8) logical_colour;

		for (int x = 0; x != SPRITE_WIDTH; ++x) {
			bitmap[x + y * SPRITE_WIDTH] = c;
		}

		/* Emit changed signal. */			
		changed ();
	}

	public void delete_row (int y, int logical_colour)
	{
		/* Check the y parameter falls on a valid row. */
		if (y >= _rows * ROW_HEIGHT) { return; }

		/* Emit will change signal. */
		will_change ();

		/* Shunt everything back a row. */
		for (int yy = y + 1; yy < _rows * ROW_HEIGHT; yy++) {
			for (int x = 0; x != SPRITE_WIDTH; ++x) {
				bitmap[x + (yy - 1) * SPRITE_WIDTH] = 
				bitmap[x + yy * SPRITE_WIDTH];
			}
		}

		/* Clear the last row. */
		var c = (uint8) logical_colour;

		for (int x = 0; x != SPRITE_WIDTH; ++x) {
			bitmap[x + (_rows * ROW_HEIGHT - 1) * SPRITE_WIDTH] = c;
		}

		/* Emit changed signal. */			
		changed ();
	}

	/**
	 * Draws a line between two points.
	 *
	 * @param from The starting position of the line.
	 * @param to The finish position of the line.
	 */
	public void draw_line (Point from, Point to, int logical_colour) {
		/* Adjust the line ends to fall inside the canvas area. */
		if (from.x >= SPRITE_WIDTH) { from.x = SPRITE_WIDTH - 1; } 
		if (from.y >= _rows * ROW_HEIGHT) { from.y = _rows * ROW_HEIGHT - 1; }
		if (to.x >= SPRITE_WIDTH) { to.x = SPRITE_WIDTH - 1; } 
		if (to.y >= _rows * ROW_HEIGHT) { to.y = _rows * ROW_HEIGHT - 1; }

		var c = (uint8) logical_colour;

		/* If the mouse hasn't moved, then no need to draw. */
		if (from.x == to.x && from.y == to.y && 
				bitmap[to.x + to.y * SPRITE_WIDTH] == c) {
			return; 
		}

		/* Emit will change signal. */
		will_change ();

		// Set pixels in a line between the old and new positions
		for (int i = 1; i <= 8; i++)
		{
			int x = from.x + ((to.x - from.x) * i + 4) / 8;
			int y = from.y + ((to.y - from.y) * i + 4) / 8;
			if (x >= 0 && x < SPRITE_WIDTH && y >= 0 && y < _rows * ROW_HEIGHT)
			{
				bitmap[x + y * SPRITE_WIDTH] = c;
			}
		}

		/* Emit changed signal. */			
		changed ();
	}
}
